/*
Copyright (c) 2008 Nikhil Belsare

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package in.belsare.scheduler;

import in.belsare.scheduler.plugin.SchedulerPlugin;

import java.io.File;
import java.util.Iterator;
import java.util.List;

import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.PropertiesConfiguration;

import com.jtheory.jdring.AlarmEntry;
import com.jtheory.jdring.AlarmListener;
import com.jtheory.jdring.AlarmManager;
import com.jtheory.jdring.PastDateException;

/**
 * JavaScheduler main class
 * @author Nikhil
 */
public class JavaScheduler implements Runnable {
	final public static String DEFAULT_CRON_FILE = "javacron.tab";
	final public static String DEFAULT_LOG_FILE = "javacron.log";
	final public static String JAVACRON_TAB_ENV = "JAVACRON_TAB";
	
	final public static int MIN_WORKER_THREADS = 5;
	final public static int MAX_WORKER_THREADS = 100;
	
	// Minimum number of fields required in the scheduler tab entry; separated by comma
	final public static int MIN_TAB_ENTRY_LENGTH = 8;	  

	final protected SchedulerLogger logger = SchedulerLogger.getInstance();
	
	protected SchedulerSocketCommandInterface ssci;
	protected AlarmManager mgr = new AlarmManager();
	protected PropertiesConfiguration config = null;
	protected String configFile;
	protected int port;
	protected String logFile;
	protected int manifestCounter;
	protected final InternalSchedulerListener listener;
	protected final WorkerPool pool;  

	public JavaScheduler() throws SchedulerException { 
		this.port = 12525;
		this.logFile = "javacron.log";
		this.configFile = "javacron.tab";
		this.manifestCounter = 0;
		this.listener = new InternalSchedulerListener(this, logger);
		this.pool = new WorkerPool(MIN_TAB_ENTRY_LENGTH, MAX_WORKER_THREADS);
	}

	public JavaScheduler(String configFile) throws SchedulerException {
		this();
		this.configFile = configFile;
		logger.info("JavaCron scheduler started");
		loadConfig();		
	}

	public JavaScheduler(String configFile, int port) throws SchedulerException {
		this(configFile);
		this.port = port;
	}

	protected void loadConfig() throws SchedulerException {
		try {
			this.config = new PropertiesConfiguration();
			this.config.load(configFile);
		} catch (ConfigurationException ce) {
			logger.warning(ce.getMessage());
		}
		if(this.config == null || !(new File(configFile)).exists())
			throw new SchedulerException("Invalid configuration file: " + configFile);
	}

	protected void loadJavaCronTab() {
		for(Iterator keys = this.config.getKeys(); keys.hasNext();) {
			String key = (String)keys.next();
			if(key.startsWith("@")) {
				processDirective(key, config);
				// Since it's a directive, skip further steps
				continue;
			}

			// It's a schedule entry
			final String[] taskDetails = this.config.getStringArray(key);
			scheduleEntry(key, taskDetails);
		}
	}

	protected void scheduleEntry(String key, String[] taskDetails) {
		if(taskDetails.length < MIN_TAB_ENTRY_LENGTH) {
			// We don't have enough info. skip this entry after crying
			logger.warning("Ignoring entry (INSUFF_ARGS): " + key);
			return;
		}

		// Load task details
		final String taskType = taskDetails[0];
		final String taskClasses[] = taskDetails[1].split(";");
		final String minutes[] = taskDetails[2].split(";");
		final String hours[] = taskDetails[3].split(";");
		final String daysOfMonth[] = taskDetails[4].split(";");
		final String months[] = taskDetails[5].split(";");
		final String daysOfWeek[] = taskDetails[6].split(";");
		final String years[] = taskDetails[7].split(";");

		/* By default scheduler static 'run' method in
		 * the given class. To override, an extra parameter
		 * in the scheduler tab entry can be appended.
		 */
		String taskMethod2 = "run";				
		if(taskDetails.length > MIN_TAB_ENTRY_LENGTH) {
			taskMethod2 = taskDetails[8].trim();
		}

		final String taskMethod = taskMethod2;

		int minute, hour, dayOfMonth, month, dayOfWeek, year;
		try {
			// For each year in the set
			for(String y: years) { 
				year = Integer.parseInt(y);
				// For each day of week in the set
				for(String dw : daysOfWeek) {
					dayOfWeek = Integer.parseInt(dw);
					// For each month in the set
					for(String m : months) {
						month = Integer.parseInt(m);
						// For each day of month in the set
						for(String dm : daysOfMonth) {
							dayOfMonth = Integer.parseInt(dm);
							// For each hour in the set
							for(String h : hours){
								hour = Integer.parseInt(h);
								// For every minute in the set
								for(String mi : minutes) {
									minute = Integer.parseInt(mi);
									//-------------------------------------------------------------------------
									//------------------------------------------------------------------------- 
									listener.newTask(key, taskType, minute, hour, dayOfMonth, month, dayOfWeek, year);
									this.mgr.addAlarm(key, minute, hour, dayOfMonth, month, dayOfWeek, year, new AlarmListener() {					
										public void handleAlarm(AlarmEntry entry) {
											int myManifestationId = nextManifestId();
											listener.taskStarted(myManifestationId, entry.getName());
											SchedulerTask task = null;
											if(taskType.equalsIgnoreCase("p")) {
												task = new SchedulerTask(myManifestationId, taskClasses[0], listener);
											}else if(taskType.equalsIgnoreCase("j")) {
												task = new SchedulerTask(myManifestationId,taskClasses, taskMethod, listener); 
											}
											if(task != null)
												pool.execute(task);
										}
									});									
									//-------------------------------------------------------------------------
									//-------------------------------------------------------------------------
								} // Minute
							} // Hour								
						} // Day of Month
					} // Month
				} // Day of Week
			} // Year
		} catch (PastDateException e) {
			logger.severe(e.getMessage());
		}
	
	}
	
	protected void processDirective(String key, PropertiesConfiguration cfg) {
		String value = cfg.getString(key);
		if(key.startsWith("@logFile")) {
			this.logFile = value;
			SchedulerLogger.getInstance().setOutputFile(value);
		}else if(key.startsWith("@port")) {
			if(value != null) {
				this.port = Integer.parseInt(value);
				// If it's a hot change. kill existing monitors
				stopTcpListener();
				// Finally start the Tcp lisjtener
				startTcpListener();
			}
		}else if(key.startsWith("@plugin")) {
			String values[] = cfg.getStringArray(key);
			loadPlugin(key, values);
		}
	}
	
	protected void loadPlugin(String key, String[] params) {
		String pluginClass = this.config.getString(key);
		try {
			Class c = Class.forName(pluginClass);
			Object newObj = c.newInstance();
			
			if(!(newObj instanceof SchedulerPlugin)) {
				logger.warning(pluginClass + " must extend " + SchedulerPlugin.class.getCanonicalName());
				return;
			}
			
			SchedulerPlugin plugin = (SchedulerPlugin)newObj;
			// Populate all properties
			for(String param : params) {
				if(param.equals(pluginClass))
					continue;
				String[] parts = param.split(":");
				if(parts.length >= 2) {
					plugin.addConfig(parts[0], parts[1]);
				}else {
					logger.warning("Plugin Warning: Invalid config parameter " + param);
				}
			}
			logger.info("Plugin: Loaded plugin " + plugin.getName());
			// Now ask internal listener to keep track of this plugin
			this.listener.addListener(plugin);
		} catch (ClassNotFoundException e) {
			logger.severe("Plugin Error: Could not find class " + pluginClass);
		} catch (InstantiationException e) {
			logger.severe("Plugin Error: Could not instantiate class " + pluginClass);
		} catch (IllegalAccessException e) {
			logger.severe("Plugin Error: " + e.getMessage());
		}
	}
	
	final protected synchronized int nextManifestId() {
		return manifestCounter++;
	}

	public void reloadAlarms() {
		try {
			this.config.reload();
			this.mgr.removeAllAlarms();
			this.loadJavaCronTab();
		} catch (Exception e) {
			logger.severe(e.getMessage());
		}
	}
	
	public void shutdown() {
		this.mgr.removeAllAlarmsAndStop();
		this.pool.kill();
	}

	public List list() {
		return this.mgr.getAllAlarms();
	}

	public void run() {
		startTcpListener();
		loadJavaCronTab();
	}
	
	public int activeTasks() {
		return pool.getNumberOfThreadsInUse();
	}


	protected void startTcpListener() {
		// if port is -1, tcp listener is disabled.
		if(this.port == -1) return;
		
		try {
			ssci = new SchedulerSocketCommandInterface(this, this.port);						
			ssci.setDaemon(true);
			ssci.start();
		}catch(Exception e) {
			logger.severe("Error starting Tcp listener on port " + port + " because: " + "\n" +
					e.getMessage());
		}
	}

	protected void stopTcpListener() {
		if(ssci != null) {
			ssci.kill();
			try { ssci.wait(); } catch(InterruptedException e) { }
			ssci = null;
		}
	}

	public static void main(String[] args) {
		String javaCronFile = null;
		if(args.length >= 1) {
			javaCronFile = args[0];
		} else {
			javaCronFile = System.getenv(JAVACRON_TAB_ENV);
		}
		
		if(javaCronFile == null)
			javaCronFile = "/etc/" + DEFAULT_CRON_FILE;
		
		try {
			System.out.println("Starting scheduler...");
			JavaScheduler js = new JavaScheduler(javaCronFile);
			Thread sThread = new Thread(js);
			sThread.setDaemon(true);
			sThread.start();
			System.out.println("Returning to shell...");
		} catch (SchedulerException e) {
			e.printStackTrace();
		}
	}
}
